ปลดล็อกความปลอดภัยของแอปพลิเคชันที่แข็งแกร่งด้วยคู่มือฉบับสมบูรณ์ของเราเกี่ยวกับการอนุญาตที่ปลอดภัยต่อชนิดข้อมูล เรียนรู้การนำระบบสิทธิ์ที่ปลอดภัยต่อชนิดข้อมูลไปใช้เพื่อป้องกันข้อผิดพลาด ปรับปรุงประสบการณ์นักพัฒนา และสร้างการควบคุมการเข้าถึงที่ปรับขนาดได้
เสริมความแข็งแกร่งให้กับโค้ดของคุณ: เจาะลึกการอนุญาตและการจัดการสิทธิ์ที่ปลอดภัยต่อชนิดข้อมูล
ในโลกที่ซับซ้อนของการพัฒนาซอฟต์แวร์ ความปลอดภัยไม่ใช่คุณสมบัติ แต่เป็นข้อกำหนดพื้นฐาน เราสร้างไฟร์วอลล์ เข้ารหัสข้อมูล และป้องกันการโจมตี แต่ช่องโหว่ทั่วไปและร้ายกาจมักแฝงตัวอยู่ในสายตา ในส่วนลึกของตรรกะแอปพลิเคชันของเรา: การอนุญาต โดยเฉพาะอย่างยิ่ง วิธีที่เราจัดการสิทธิ์ หลายปีที่ผ่านมา นักพัฒนาได้พึ่งพารูปแบบที่ดูเหมือนไม่มีพิษภัย นั่นคือ สิทธิ์แบบสตริง ซึ่งเป็นแนวทางปฏิบัติที่แม้ว่าจะเริ่มต้นได้ง่าย แต่ก็มักนำไปสู่ระบบที่เปราะบาง เกิดข้อผิดพลาดได้ง่าย และไม่ปลอดภัย จะเป็นอย่างไรหากเราสามารถใช้ประโยชน์จากเครื่องมือพัฒนาของเราเพื่อตรวจจับข้อผิดพลาดในการอนุญาตก่อนที่จะเข้าสู่ขั้นตอนการผลิต จะเป็นอย่างไรหากคอมไพเลอร์เองสามารถเป็นแนวป้องกันแรกของเราได้ ยินดีต้อนรับสู่โลกของ การอนุญาตที่ปลอดภัยต่อชนิดข้อมูล
คู่มือนี้จะนำคุณไปสู่การเดินทางที่ครอบคลุมจากโลกที่เปราะบางของสิทธิ์แบบสตริง ไปสู่การสร้างระบบการอนุญาตที่ปลอดภัยต่อชนิดข้อมูลที่แข็งแกร่ง บำรุงรักษาได้ และมีความปลอดภัยสูง เราจะสำรวจ 'ทำไม' 'อะไร' และ 'อย่างไร' โดยใช้ตัวอย่างเชิงปฏิบัติใน TypeScript เพื่อแสดงแนวคิดที่สามารถนำไปใช้ได้กับภาษาที่พิมพ์แบบคงที่ใดๆ เมื่อถึงจุดสิ้นสุด คุณจะไม่เพียงแต่เข้าใจทฤษฎีเท่านั้น แต่ยังมีความรู้เชิงปฏิบัติในการนำระบบการจัดการสิทธิ์ไปใช้ ซึ่งจะช่วยเสริมสร้างความปลอดภัยของแอปพลิเคชันของคุณและเพิ่มประสิทธิภาพประสบการณ์นักพัฒนาของคุณ
ความเปราะบางของสิทธิ์แบบสตริง: ข้อผิดพลาดทั่วไป
โดยแก่นแท้แล้ว การอนุญาตคือการตอบคำถามง่ายๆ: "ผู้ใช้รายนี้มีสิทธิ์ดำเนินการนี้หรือไม่" วิธีที่ตรงไปตรงมาที่สุดในการแสดงสิทธิ์คือสตริง เช่น "edit_post" หรือ "delete_user" สิ่งนี้นำไปสู่โค้ดที่มีลักษณะดังนี้:
if (user.hasPermission("create_product")) { ... }
แนวทางนี้ง่ายต่อการนำไปใช้ในตอนแรก แต่เป็นบ้านที่สร้างจากไพ่ แนวทางปฏิบัตินี้ มักเรียกกันว่าการใช้ "magic strings" นำมาซึ่งความเสี่ยงและหนี้ทางเทคนิคจำนวนมาก ลองวิเคราะห์ว่าทำไมรูปแบบนี้ถึงมีปัญหามาก
ลำดับข้อผิดพลาด
- การพิมพ์ผิดแบบเงียบ: นี่คือปัญหาที่เห็นได้ชัดเจนที่สุด การพิมพ์ผิดง่ายๆ เช่น การตรวจสอบ
"create_pruduct"แทนที่จะเป็น"create_product"จะไม่ทำให้เกิดข้อขัดข้อง จะไม่แจ้งเตือนด้วยซ้ำ การตรวจสอบจะล้มเหลวอย่างเงียบๆ และผู้ใช้ที่ ควร มีสิทธิ์เข้าถึงจะถูกปฏิเสธ ที่แย่กว่านั้น การพิมพ์ผิดในคำจำกัดความสิทธิ์อาจให้สิทธิ์เข้าถึงโดยไม่ได้ตั้งใจในที่ที่ไม่ควร ข้อผิดพลาดเหล่านี้ยากต่อการติดตามอย่างไม่น่าเชื่อ - ขาดการค้นพบ: เมื่อนักพัฒนาใหม่เข้าร่วมทีม พวกเขาจะรู้ได้อย่างไรว่ามีสิทธิ์ใดบ้าง พวกเขาต้องค้นหาโค้ดเบสทั้งหมด โดยหวังว่าจะพบการใช้งานทั้งหมด ไม่มีแหล่งที่มาของความจริงเพียงแหล่งเดียว ไม่มี autocomplete และไม่มีเอกสารประกอบที่จัดทำโดยโค้ดเอง
- ฝันร้ายในการปรับโครงสร้าง: ลองจินตนาการว่าองค์กรของคุณตัดสินใจที่จะนำอนุสัญญาการตั้งชื่อที่มีโครงสร้างมากขึ้นมาใช้ โดยเปลี่ยน
"edit_post"เป็น"post:update"สิ่งนี้ต้องใช้การดำเนินการค้นหาและแทนที่แบบส่วนกลางและคำนึงถึงขนาดตัวพิมพ์ทั่วทั้งโค้ดเบสทั้งหมด ส่วนหลัง ส่วนหน้า และอาจรวมถึงรายการฐานข้อมูลด้วย เป็นกระบวนการแมนนวลที่มีความเสี่ยงสูง โดยที่อินสแตนซ์เดียวที่พลาดไปสามารถทำลายคุณสมบัติหรือสร้างช่องโหว่ด้านความปลอดภัยได้ - ไม่มีความปลอดภัยในเวลาคอมไพล์: จุดอ่อนพื้นฐานคือความถูกต้องของสตริงสิทธิ์จะถูกตรวจสอบเมื่อรันไทม์เท่านั้น คอมไพเลอร์ไม่มีความรู้ว่าสตริงใดเป็นสิทธิ์ที่ถูกต้องและสตริงใดที่ไม่ถูกต้อง มันมองว่า
"delete_user"และ"delete_useeer"เป็นสตริงที่ถูกต้องเท่าเทียมกัน โดยเลื่อนการค้นพบข้อผิดพลาดไปยังผู้ใช้ของคุณหรือขั้นตอนการทดสอบของคุณ
ตัวอย่างที่เป็นรูปธรรมของความล้มเหลว
พิจารณาบริการแบ็กเอนด์ที่ควบคุมการเข้าถึงเอกสาร สิทธิ์ในการลบเอกสารถูกกำหนดเป็น "document_delete"
นักพัฒนาที่ทำงานบนแผงควบคุมผู้ดูแลระบบจำเป็นต้องเพิ่มปุ่มลบ พวกเขาเขียนการตรวจสอบดังนี้:
// ใน API endpoint
if (currentUser.hasPermission("document:delete")) {
// ดำเนินการลบต่อ
} else {
return res.status(403).send("Forbidden");
}
นักพัฒนา ตามอนุสัญญาที่ใหม่กว่า ใช้เครื่องหมายทวิภาค (:) แทนเครื่องหมายขีดล่าง (_) โค้ดถูกต้องตามหลักไวยากรณ์และจะผ่านกฎการ linting ทั้งหมด อย่างไรก็ตาม เมื่อใช้งาน ผู้ดูแลระบบจะไม่สามารถลบเอกสารได้ คุณสมบัติเสีย แต่ระบบไม่ขัดข้อง ระบบเพียงแค่ส่งคืนข้อผิดพลาด 403 Forbidden ข้อผิดพลาดนี้อาจไม่ได้รับการสังเกตเป็นเวลาหลายวันหรือหลายสัปดาห์ ทำให้ผู้ใช้หงุดหงิดและต้องใช้เซสชันการแก้ไขจุดบกพร่องที่เจ็บปวดเพื่อค้นพบข้อผิดพลาดเพียงตัวเดียว
นี่ไม่ใช่หนทางที่ยั่งยืนหรือปลอดภัยในการสร้างซอฟต์แวร์ระดับมืออาชีพ เราต้องการแนวทางที่ดีกว่า
ขอแนะนำการอนุญาตที่ปลอดภัยต่อชนิดข้อมูล: คอมไพเลอร์ในฐานะแนวป้องกันแรกของคุณ
การอนุญาตที่ปลอดภัยต่อชนิดข้อมูลเป็นการเปลี่ยนกระบวนทัศน์ แทนที่จะแสดงสิทธิ์เป็นสตริงตามอำเภอใจที่คอมไพเลอร์ไม่รู้จัก เรากำหนดสิทธิ์เหล่านั้นเป็นชนิดที่ชัดเจนภายในระบบชนิดของภาษาโปรแกรมของเรา การเปลี่ยนแปลงง่ายๆ นี้ย้ายการตรวจสอบสิทธิ์จากความกังวลเกี่ยวกับรันไทม์ไปเป็นการ รับประกันเวลาคอมไพล์
เมื่อคุณใช้ระบบที่ปลอดภัยต่อชนิดข้อมูล คอมไพเลอร์จะเข้าใจชุดสิทธิ์ที่ถูกต้องทั้งหมด หากคุณพยายามตรวจสอบสิทธิ์ที่ไม่มีอยู่ โค้ดของคุณจะไม่คอมไพล์ด้วยซ้ำ การพิมพ์ผิดจากตัวอย่างก่อนหน้าของเรา "document:delete" เทียบกับ "document_delete" จะถูกจับได้ทันทีในตัวแก้ไขโค้ดของคุณ ขีดเส้นใต้เป็นสีแดง ก่อนที่คุณจะบันทึกไฟล์ด้วยซ้ำ
หลักการพื้นฐาน
- คำจำกัดความส่วนกลาง: สิทธิ์ที่เป็นไปได้ทั้งหมดถูกกำหนดไว้ในตำแหน่งที่ใช้ร่วมกันเพียงตำแหน่งเดียว ไฟล์หรือโมดูลนี้กลายเป็นแหล่งที่มาของความจริงที่ไม่อาจปฏิเสธได้สำหรับโมเดลความปลอดภัยของแอปพลิเคชันทั้งหมด
- การตรวจสอบเวลาคอมไพล์: ระบบชนิดช่วยให้มั่นใจได้ว่าการอ้างอิงสิทธิ์ใดๆ ไม่ว่าจะในการตรวจสอบ คำจำกัดความบทบาท หรือส่วนประกอบ UI เป็นสิทธิ์ที่ถูกต้องและมีอยู่จริง การพิมพ์ผิดและสิทธิ์ที่ไม่มีอยู่เป็นไปไม่ได้
- ประสบการณ์นักพัฒนาที่ได้รับการปรับปรุง (DX): นักพัฒนาจะได้รับคุณสมบัติ IDE เช่น autocomplete เมื่อพวกเขาพิมพ์
user.hasPermission(...)พวกเขาสามารถเห็นรายการแบบเลื่อนลงของสิทธิ์ที่มีอยู่ทั้งหมด ทำให้ระบบจัดทำเอกสารด้วยตนเองและลดค่าใช้จ่ายทางจิตในการจดจำค่าสตริงที่แน่นอน - การปรับโครงสร้างอย่างมั่นใจ: หากคุณต้องการเปลี่ยนชื่อสิทธิ์ คุณสามารถใช้เครื่องมือปรับโครงสร้างในตัวของ IDE ของคุณ การเปลี่ยนชื่อสิทธิ์ที่แหล่งที่มาจะอัปเดตการใช้งานทุกรายการทั่วทั้งโครงการโดยอัตโนมัติและปลอดภัย สิ่งที่ครั้งหนึ่งเคยเป็นงานแมนนวลที่มีความเสี่ยงสูง กลายเป็นงานที่ง่าย ปลอดภัย และเป็นอัตโนมัติ
การสร้างรากฐาน: การนำระบบสิทธิ์ที่ปลอดภัยต่อชนิดข้อมูลไปใช้
มาเปลี่ยนจากทฤษฎีสู่การปฏิบัติ เราจะสร้างระบบสิทธิ์ที่ปลอดภัยต่อชนิดข้อมูลที่สมบูรณ์ตั้งแต่เริ่มต้น สำหรับตัวอย่างของเรา เราจะใช้ TypeScript เพราะระบบชนิดที่มีประสิทธิภาพนั้นเหมาะอย่างยิ่งสำหรับงานนี้ อย่างไรก็ตาม หลักการพื้นฐานสามารถปรับให้เข้ากับภาษาที่พิมพ์แบบคงที่อื่นๆ เช่น C#, Java, Swift, Kotlin หรือ Rust ได้อย่างง่ายดาย
ขั้นตอนที่ 1: การกำหนดสิทธิ์ของคุณ
ขั้นตอนแรกและสำคัญที่สุดคือการสร้างแหล่งที่มาของความจริงเพียงแหล่งเดียวสำหรับสิทธิ์ทั้งหมด มีหลายวิธีในการบรรลุเป้าหมายนี้ ซึ่งแต่ละวิธีมีข้อดีข้อเสียของตัวเอง
ตัวเลือก A: การใช้ชนิด Union Literal สตริง
นี่เป็นแนวทางที่ง่ายที่สุด คุณกำหนดชนิดที่เป็นสหภาพของสตริงสิทธิ์ที่เป็นไปได้ทั้งหมด มีความกระชับและมีประสิทธิภาพสำหรับแอปพลิเคชันขนาดเล็ก
// src/permissions.ts
export type Permission =
| "user:create"
| "user:read"
| "user:update"
| "user:delete"
| "post:create"
| "post:read"
| "post:update"
| "post:delete";
ข้อดี: เขียนและเข้าใจง่ายมาก
ข้อเสีย: อาจเทอะทะเมื่อจำนวนสิทธิ์เพิ่มขึ้น ไม่ได้ให้วิธีในการจัดกลุ่มสิทธิ์ที่เกี่ยวข้อง และคุณยังต้องพิมพ์สตริงเมื่อใช้งาน
ตัวเลือก B: การใช้ Enums
Enums ให้วิธีในการจัดกลุ่มค่าคงที่ที่เกี่ยวข้องภายใต้ชื่อเดียว ซึ่งสามารถทำให้โค้ดของคุณอ่านง่ายขึ้น
// src/permissions.ts
export enum Permission {
UserCreate = "user:create",
UserRead = "user:read",
UserUpdate = "user:update",
UserDelete = "user:delete",
PostCreate = "post:create",
// ... และอื่นๆ
}
ข้อดี: ให้ค่าคงที่ที่มีชื่อ (Permission.UserCreate) ซึ่งสามารถป้องกันการพิมพ์ผิดเมื่อใช้สิทธิ์
ข้อเสีย: TypeScript enums มีความแตกต่างเล็กน้อยและอาจมีความยืดหยุ่นน้อยกว่าแนวทางอื่นๆ การแยกค่าสตริงสำหรับชนิด union ต้องใช้ขั้นตอนพิเศษ
ตัวเลือก C: แนวทาง Object-as-Const (แนะนำ)
นี่เป็นแนวทางที่ทรงพลังและปรับขนาดได้มากที่สุด เรากำหนดสิทธิ์ในวัตถุที่ซ้อนกันอย่างลึกซึ้งและอ่านอย่างเดียวโดยใช้การยืนยัน `as const` ของ TypeScript สิ่งนี้ทำให้เราได้สิ่งที่ดีที่สุดจากทุกโลก: การจัดระเบียบ การค้นพบผ่านสัญกรณ์จุด (เช่น `Permissions.USER.CREATE`) และความสามารถในการสร้างชนิด union ของสตริงสิทธิ์ทั้งหมดแบบไดนามิก
นี่คือวิธีการตั้งค่า:
// src/permissions.ts
// 1. กำหนดวัตถุสิทธิ์ด้วย 'as const'
export const Permissions = {
USER: {
CREATE: "user:create",
READ: "user:read",
UPDATE: "user:update",
DELETE: "user:delete",
},
POST: {
CREATE: "post:create",
READ: "post:read",
UPDATE: "post:update",
DELETE: "post:delete",
},
BILLING: {
READ_INVOICES: "billing:read_invoices",
MANAGE_SUBSCRIPTION: "billing:manage_subscription",
}
} as const;
// 2. สร้างชนิด helper เพื่อแยกค่าสิทธิ์ทั้งหมด
type TPermissions = typeof Permissions;
// ชนิดยูทิลิตี้นี้จะทำให้ค่าวัตถุที่ซ้อนกันแบนราบเป็น union แบบเรียกซ้ำ
type FlattenObjectValues
แนวทางนี้เหนือกว่าเพราะให้โครงสร้างที่ชัดเจนและเป็นลำดับชั้นสำหรับสิทธิ์ของคุณ ซึ่งมีความสำคัญอย่างยิ่งเมื่อแอปพลิเคชันของคุณเติบโต ง่ายต่อการเรียกดู และชนิด `AllPermissions` จะถูกสร้างขึ้นโดยอัตโนมัติ ซึ่งหมายความว่าคุณไม่จำเป็นต้องอัปเดตชนิด union ด้วยตนเอง นี่คือรากฐานที่เราจะใช้สำหรับระบบที่เหลือของเรา
ขั้นตอนที่ 2: การกำหนดบทบาท
บทบาทคือชุดของสิทธิ์ที่มีชื่อ เราสามารถใช้ชนิด `AllPermissions` ของเราเพื่อให้แน่ใจว่าคำจำกัดความบทบาทของเรานั้นปลอดภัยต่อชนิดข้อมูลเช่นกัน
// src/roles.ts
import { Permissions, AllPermissions } from './permissions';
// กำหนดโครงสร้างสำหรับบทบาท
export type Role = {
name: string;
description: string;
permissions: AllPermissions[];
};
// กำหนดเรคคอร์ดของบทบาทแอปพลิเคชันทั้งหมด
export const AppRoles: Record
โปรดสังเกตว่าเรากำลังใช้วัตถุ `Permissions` (เช่น `Permissions.POST.READ`) เพื่อกำหนดสิทธิ์ สิ่งนี้ป้องกันการพิมพ์ผิดและทำให้แน่ใจว่าเรากำลังกำหนดสิทธิ์ที่ถูกต้องเท่านั้น สำหรับบทบาท `ADMIN` เราทำให้วัตถุ `Permissions` ของเราแบนราบโดยทางโปรแกรมเพื่อให้สิทธิ์ทั้งหมด ทำให้มั่นใจได้ว่าเมื่อมีการเพิ่มสิทธิ์ใหม่ ผู้ดูแลระบบจะสืบทอดสิทธิ์เหล่านั้นโดยอัตโนมัติ
ขั้นตอนที่ 3: การสร้างฟังก์ชัน Checker ที่ปลอดภัยต่อชนิดข้อมูล
นี่คือหัวใจสำคัญของระบบของเรา เราต้องการฟังก์ชันที่สามารถตรวจสอบได้ว่าผู้ใช้มีสิทธิ์เฉพาะหรือไม่ สิ่งสำคัญคือลายเซ็นของฟังก์ชัน ซึ่งจะบังคับใช้ว่าสามารถตรวจสอบได้เฉพาะสิทธิ์ที่ถูกต้องเท่านั้น
ขั้นแรก มากำหนดว่าวัตถุ `User` อาจมีลักษณะอย่างไร:
// src/user.ts
import { AppRoleKey } from './roles';
export type User = {
id: string;
email: string;
roles: AppRoleKey[]; // บทบาทของผู้ใช้ก็ปลอดภัยต่อชนิดข้อมูลเช่นกัน!
};
มาสร้างตรรกะการอนุญาตกัน เพื่อประสิทธิภาพ ควรคำนวณชุดสิทธิ์ทั้งหมดของผู้ใช้หนึ่งครั้ง แล้วจึงตรวจสอบกับชุดนั้น
// src/authorization.ts
import { User } from './user';
import { AppRoles } from './roles';
import { AllPermissions } from './permissions';
/**
* คำนวณชุดสิทธิ์ที่สมบูรณ์สำหรับผู้ใช้ที่กำหนด
* ใช้ Set สำหรับการค้นหา O(1) ที่มีประสิทธิภาพ
* @param user วัตถุผู้ใช้
* @returns Set ที่มีสิทธิ์ทั้งหมดที่ผู้ใช้มี
*/
function getUserPermissions(user: User): Set
ความมหัศจรรย์อยู่ที่พารามิเตอร์ `permission: AllPermissions` ของฟังก์ชัน `hasPermission` ลายเซ็นนี้บอกคอมไพเลอร์ TypeScript ว่าอาร์กิวเมนต์ที่สอง ต้อง เป็นหนึ่งในสตริงจากชนิด union `AllPermissions` ที่สร้างขึ้นของเรา ความพยายามใดๆ ในการใช้สตริงอื่นจะส่งผลให้เกิดข้อผิดพลาดในเวลาคอมไพล์
การใช้งานในทางปฏิบัติ
มาดูกันว่าสิ่งนี้เปลี่ยนการเขียนโค้ดประจำวันของเราอย่างไร ลองจินตนาการถึงการปกป้อง API endpoint ในแอปพลิเคชัน Node.js/Express:
import { hasPermission } from './authorization';
import { Permissions } from './permissions';
import { User } from './user';
app.delete('/api/posts/:id', (req, res) => {
const currentUser: User = req.user; // สันนิษฐานว่าผู้ใช้ถูกแนบมาจากมิดเดิลแวร์การตรวจสอบสิทธิ์
// สิ่งนี้ใช้งานได้อย่างสมบูรณ์! เราได้รับ autocomplete สำหรับ Permissions.POST.DELETE
if (hasPermission(currentUser, Permissions.POST.DELETE)) {
// ตรรกะในการลบโพสต์
res.status(200).send({ message: 'Post deleted.' });
} else {
res.status(403).send({ error: 'You do not have permission to delete posts.' });
}
});
// ตอนนี้ ลองทำผิดพลาด:
app.post('/api/users', (req, res) => {
const currentUser: User = req.user;
// บรรทัดต่อไปนี้จะแสดงเส้นหยักสีแดงใน IDE ของคุณและล้มเหลวในการคอมไพล์!
// ข้อผิดพลาด: Argument of type '"user:creat"' is not assignable to parameter of type 'AllPermissions'.
// Did you mean '"user:create"'?
if (hasPermission(currentUser, "user:creat")) { // พิมพ์ผิดใน 'create'
// โค้ดนี้เข้าถึงไม่ได้
}
});
เราได้กำจัดข้อผิดพลาดประเภทหนึ่งออกไปเรียบร้อยแล้ว ตอนนี้คอมไพเลอร์มีส่วนร่วมอย่างแข็งขันในการบังคับใช้โมเดลความปลอดภัยของเรา
การปรับขนาดระบบ: แนวคิดขั้นสูงในการอนุญาตที่ปลอดภัยต่อชนิดข้อมูล
ระบบควบคุมการเข้าถึงตามบทบาท (RBAC) อย่างง่ายนั้นมีประสิทธิภาพ แต่แอปพลิเคชันในโลกแห่งความเป็นจริงมักมีความต้องการที่ซับซ้อนกว่า เราจะจัดการกับสิทธิ์ที่ขึ้นอยู่กับข้อมูลได้อย่างไร ตัวอย่างเช่น `EDITOR` สามารถอัปเดตโพสต์ได้ แต่เฉพาะโพสต์ ของตนเอง เท่านั้น
การควบคุมการเข้าถึงตามแอตทริบิวต์ (ABAC) และสิทธิ์ตามทรัพยากร
นี่คือที่ที่เราแนะนำแนวคิดของการควบคุมการเข้าถึงตามแอตทริบิวต์ (ABAC) เราขยายระบบของเราเพื่อจัดการกับนโยบายหรือเงื่อนไข ผู้ใช้จะต้องไม่เพียงแต่มีสิทธิ์ทั่วไป (เช่น `post:update`) เท่านั้น แต่ยังต้องปฏิบัติตามกฎที่เกี่ยวข้องกับทรัพยากรเฉพาะที่พวกเขากำลังพยายามเข้าถึงด้วย
เราสามารถจำลองสิ่งนี้ได้ด้วยแนวทางที่อิงตามนโยบาย เรากำหนดแผนที่ของนโยบายที่สอดคล้องกับสิทธิ์บางอย่าง
// src/policies.ts
import { User } from './user';
// กำหนดชนิดทรัพยากรของเรา
type Post = { id: string; authorId: string; };
// กำหนดแผนที่ของนโยบาย คีย์คือนโยบายที่ปลอดภัยต่อชนิดข้อมูลของเรา!
type PolicyMap = {
[Permissions.POST.UPDATE]?: (user: User, post: Post) => boolean;
[Permissions.POST.DELETE]?: (user: User, post: Post) => boolean;
// นโยบายอื่นๆ...
};
export const policies: PolicyMap = {
[Permissions.POST.UPDATE]: (user, post) => {
// ในการอัปเดตโพสต์ ผู้ใช้ต้องเป็นผู้เขียน
return user.id === post.authorId;
},
[Permissions.POST.DELETE]: (user, post) => {
// ในการลบโพสต์ ผู้ใช้ต้องเป็นผู้เขียน
return user.id === post.authorId;
},
};
// เราสามารถสร้างฟังก์ชันตรวจสอบใหม่ที่ทรงพลังยิ่งขึ้น
export function can(user: User | null, permission: AllPermissions, resource?: any): boolean {
if (!user) return false;
// 1. ขั้นแรก ตรวจสอบว่าผู้ใช้มีสิทธิ์พื้นฐานจากบทบาทของตนหรือไม่
if (!hasPermission(user, permission)) {
return false;
}
// 2. ถัดไป ตรวจสอบว่ามีนโยบายเฉพาะสำหรับสิทธิ์นี้หรือไม่
const policy = policies[permission];
if (policy) {
// 3. หากมีนโยบาย จะต้องเป็นไปตามนั้น
if (!resource) {
// นโยบายต้องการทรัพยากร แต่ไม่มีให้
console.warn(`Policy for ${permission} was not checked because no resource was provided.`);
return false;
}
return policy(user, resource);
}
// 4. หากไม่มีนโยบาย การมีสิทธิ์ตามบทบาทก็เพียงพอแล้ว
return true;
}
ตอนนี้ API endpoint ของเรามีความแตกต่างและปลอดภัยยิ่งขึ้น:
import { can } from './policies';
import { Permissions } from './permissions';
app.put('/api/posts/:id', async (req, res) => {
const currentUser = req.user;
const post = await db.posts.findById(req.params.id);
// ตรวจสอบความสามารถในการอัปเดตโพสต์ *เฉพาะ* นี้
if (can(currentUser, Permissions.POST.UPDATE, post)) {
// ผู้ใช้มีสิทธิ์ 'post:update' และเป็นผู้เขียน
// ดำเนินการตรรกะการอัปเดตต่อ...
} else {
res.status(403).send({ error: 'You are not authorized to update this post.' });
}
});
การรวมส่วนหน้า: การแชร์ชนิดระหว่างแบ็กเอนด์และส่วนหน้า
ข้อดีที่สำคัญที่สุดอย่างหนึ่งของแนวทางนี้ โดยเฉพาะอย่างยิ่งเมื่อใช้ TypeScript ทั้งในส่วนหน้าและแบ็กเอนด์ คือความสามารถในการแชร์ชนิดเหล่านี้ โดยการวาง `permissions.ts` `roles.ts` และไฟล์ที่ใช้ร่วมกันอื่นๆ ไว้ในแพ็กเกจทั่วไปภายใน monorepo (โดยใช้เครื่องมือเช่น Nx, Turborepo หรือ Lerna) แอปพลิเคชันส่วนหน้าของคุณจะรับรู้ถึงโมเดลการอนุญาตอย่างเต็มที่
สิ่งนี้ทำให้เกิดรูปแบบที่มีประสิทธิภาพในโค้ด UI ของคุณ เช่น การเรนเดอร์องค์ประกอบแบบมีเงื่อนไขตามสิทธิ์ของผู้ใช้ ทั้งหมดนี้มีความปลอดภัยของระบบชนิด
พิจารณาส่วนประกอบ React:
// ในส่วนประกอบ React
import { Permissions } from '@my-app/shared-types'; // การนำเข้าจากแพ็กเกจที่ใช้ร่วมกัน
import { useAuth } from './auth-context'; // ฮุกแบบกำหนดเองสำหรับสถานะการตรวจสอบสิทธิ์
interface EditPostButtonProps {
post: Post;
}
const EditPostButton = ({ post }: EditPostButtonProps) => {
const { user, can } = useAuth(); // 'can' คือฮุกที่ใช้ตรรกะตามนโยบายใหม่ของเรา
// การตรวจสอบนั้นปลอดภัยต่อชนิดข้อมูล UI รู้เกี่ยวกับสิทธิ์และนโยบาย!
if (!can(user, Permissions.POST.UPDATE, post)) {
return null; // ไม่ต้องเรนเดอร์ปุ่มด้วยซ้ำ หากผู้ใช้ไม่สามารถดำเนินการได้
}
return ;
};
นี่คือตัวเปลี่ยนเกม โค้ดส่วนหน้าของคุณไม่จำเป็นต้องคาดเดาหรือใช้สตริงที่ฮาร์ดโค้ดเพื่อควบคุมการมองเห็น UI อีกต่อไป ซิงโครไนซ์กับโมเดลความปลอดภัยของแบ็กเอนด์อย่างสมบูรณ์แบบ และการเปลี่ยนแปลงใดๆ ในสิทธิ์บนแบ็กเอนด์จะทำให้เกิดข้อผิดพลาดของชนิดบนส่วนหน้าทันทีหากไม่ได้อัปเดต ป้องกันความไม่สอดคล้องกันของ UI
กรณีทางธุรกิจ: ทำไมองค์กรของคุณจึงควรลงทุนในการอนุญาตที่ปลอดภัยต่อชนิดข้อมูล
การนำรูปแบบนี้มาใช้เป็นมากกว่าการปรับปรุงทางเทคนิค เป็นการลงทุนเชิงกลยุทธ์ที่มีผลประโยชน์ทางธุรกิจที่จับต้องได้
- ลดข้อผิดพลาดอย่างมาก: กำจัดช่องโหว่ด้านความปลอดภัยและข้อผิดพลาดรันไทม์ที่เกี่ยวข้องกับการอนุญาตอย่างสมบูรณ์ สิ่งนี้แปลเป็นผลิตภัณฑ์ที่เสถียรยิ่งขึ้นและเหตุการณ์การผลิตที่มีค่าใช้จ่ายน้อยลง
- เร่งความเร็วในการพัฒนา: Autocomplete การวิเคราะห์แบบคงที่ และโค้ดที่จัดทำเอกสารด้วยตนเองทำให้นักพัฒนารวดเร็วและมั่นใจยิ่งขึ้น ใช้เวลาน้อยลงในการค้นหาสตริงสิทธิ์หรือแก้ไขจุดบกพร่องความล้มเหลวในการอนุญาตแบบเงียบๆ
- ลดความซับซ้อนในการเริ่มต้นใช้งานและการบำรุงรักษา: ระบบสิทธิ์ไม่ใช่ความรู้ของชนเผ่าอีกต่อไป นักพัฒนาใหม่สามารถเข้าใจโมเดลความปลอดภัยได้ทันทีโดยการตรวจสอบชนิดที่ใช้ร่วมกัน การบำรุงรักษาและการปรับโครงสร้างกลายเป็นงานที่มีความเสี่ยงต่ำและคาดเดาได้
- ปรับปรุงท่าทางความปลอดภัย: ระบบสิทธิ์ที่ชัดเจน ชัดเจน และจัดการจากส่วนกลางนั้นง่ายต่อการตรวจสอบและให้เหตุผลมากขึ้น สามารถตอบคำถามเช่น "ใครมีสิทธิ์ลบผู้ใช้" ได้อย่างง่ายดาย สิ่งนี้เสริมสร้างการปฏิบัติตามข้อกำหนดและการตรวจสอบความปลอดภัย
ความท้าทายและข้อควรพิจารณา
แม้ว่าจะมีประสิทธิภาพ แต่แนวทางนี้ก็ไม่ใช่ว่าจะไม่มีข้อควรพิจารณา:
- ความซับซ้อนในการตั้งค่าเริ่มต้น: ต้องใช้ความคิดทางสถาปัตยกรรมล่วงหน้ามากกว่าการกระจายการตรวจสอบสตริงไปทั่วโค้ดของคุณ อย่างไรก็ตาม การลงทุนเริ่มต้นนี้จะให้ผลตอบแทนตลอดวงจรชีวิตของโครงการ
- ประสิทธิภาพในระดับ: ในระบบที่มีสิทธิ์หลายพันรายการหรือลำดับชั้นผู้ใช้ที่ซับซ้อนมาก กระบวนการคำนวณชุดสิทธิ์ของผู้ใช้ (`getUserPermissions`) อาจกลายเป็นคอขวดได้ ในสถานการณ์ดังกล่าว การนำกลยุทธ์การแคชไปใช้ (เช่น การใช้ Redis เพื่อจัดเก็บชุดสิทธิ์ที่คำนวณ) เป็นสิ่งสำคัญ
- เครื่องมือและการสนับสนุนภาษา: ผลประโยชน์ทั้งหมดของแนวทางนี้เกิดขึ้นในภาษาที่มีระบบการพิมพ์แบบคงที่ที่แข็งแกร่ง แม้ว่าจะสามารถประมาณค่าในภาษาที่พิมพ์แบบไดนามิกเช่น Python หรือ Ruby ได้ด้วยการบอกใบ้ชนิดและเครื่องมือวิเคราะห์แบบคงที่ แต่ส่วนใหญ่เป็นภาษาแม่เช่น TypeScript, C#, Java และ Rust
สรุป: การสร้างอนาคตที่ปลอดภัยและบำรุงรักษาได้มากขึ้น
เราได้เดินทางจากภูมิประเทศที่ทรยศของ magic strings ไปยังเมืองที่มีป้อมปราการอย่างดีของการอนุญาตที่ปลอดภัยต่อชนิดข้อมูล โดยการปฏิบัติต่อสิทธิ์ไม่ใช่ข้อมูลอย่างง่าย แต่เป็นส่วนสำคัญของระบบชนิดของแอปพลิเคชันของเรา เราเปลี่ยนคอมไพเลอร์จากตัวตรวจสอบโค้ดอย่างง่ายๆ ให้กลายเป็นการ์ดรักษาความปลอดภัยที่ระมัดระวัง
การอนุญาตที่ปลอดภัยต่อชนิดข้อมูลเป็นเครื่องพิสูจน์ถึงหลักการวิศวกรรมซอฟต์แวร์สมัยใหม่ของการเปลี่ยนไปทางซ้าย ซึ่งเป็นการจับข้อผิดพลาดให้เร็วที่สุดเท่าที่จะเป็นไปได้ในวงจรชีวิตการพัฒนา เป็นการลงทุนเชิงกลยุทธ์ในคุณภาพของโค้ด ประสิทธิภาพการทำงานของนักพัฒนา และที่สำคัญที่สุดคือความปลอดภัยของแอปพลิเคชัน ด้วยการสร้างระบบที่จัดทำเอกสารด้วยตนเอง ง่ายต่อการปรับโครงสร้าง และเป็นไปไม่ได้ที่จะใช้ในทางที่ผิด คุณไม่ได้เขียนโค้ดที่ดีกว่าเท่านั้น คุณกำลังสร้างอนาคตที่ปลอดภัยและบำรุงรักษาได้มากขึ้นสำหรับแอปพลิเคชันและทีมของคุณ ในครั้งต่อไปที่คุณเริ่มต้นโครงการใหม่หรือมองหาการปรับโครงสร้างโครงการเก่า ให้ถามตัวเองว่า: ระบบการอนุญาตของคุณทำงานเพื่อคุณหรือต่อต้านคุณ